#include "library.h"
#include "pdftool.h"

const QString libName = ".fundamental";

bool runCommand(QString application, QStringList args, QString dir, QString &message, QString *output = NULL);

Library::Library(QString path, bool autoGit, QObject *parent) :
	QObject(parent),
	path(path),
	autoGit(autoGit)
{
}

Library::~Library()
{
}

bool Library::init(QString &message, bool &fatal, bool &errors)
{
	fatal = false;
	errors = false;
	message.clear();
	if (autoGit) {
		if (!gitPull(message)) {
			fatal = false;
			errors = true;
			message += "Disabling git support.";
			autoGit = false;
		}
	}

	if (!load() && QFile(getLibFileName()).exists()) {
		message = "Could not load library!";
		fatal = true;
		return false;
	}

	{
		bool getPageCount = true;
		foreach (QByteArray docHash, docs.keys()) {
			if (getFileData(docHash, "Page count").toInt() > 0) {
				getPageCount = false;
				break;
			}
		}
		if (getPageCount) {
			foreach (QByteArray docHash, docs.keys()) {
				if (getFileData(docHash, "Page count").toInt() == 0) {
					PDFTool tool(getDocFileName(docHash));
					setFileData(docHash, "Page count", QString::number(tool.pageCount()));
				}
			}
		}
	}
	{
		bool getFullText = true;
		foreach (QByteArray docHash, docs.keys()) {
			if (!docs[docHash].fullText.isEmpty()) {
				getFullText = false;
				break;
			}
		}
		if (getFullText) {
			foreach (QByteArray docHash, docs.keys()) {
				if (docs[docHash].fullText.isEmpty()) {
					PDFTool tool(getDocFileName(docHash));
					docs[docHash].fullText = tool.fullText();
				}
			}
		}
	}

	save();

	return true;
}

bool Library::destroy(QString &message)
{
	message.clear();
	save();

	if (autoGit) {
		if (!gitCommitAndPush(message)) {
			return false;
		}
	}
	return true;
}

bool Library::load()
{
	QFile file(getLibFileName());
	if (!file.open(QIODevice::ReadOnly)) {
		qDebug() << __FILE__ << __LINE__ << "Failed to open file:" << file.fileName();
		return false;
	}
	QDataStream in(&file);
	in.setVersion(QDataStream::Qt_4_0);

	in >> *this;
	if (in.status() != QDataStream::Ok)
		return false;

	return true;
}

bool Library::save()
{
	if (path.isEmpty())
		return false;

	QDir dir("/");
	dir.mkpath(path);
	dir.mkpath(getDownloadsPath());
	dir.mkpath(getDeletedPath());
	dir.mkpath(getMetadataBackupPath());
	{
		QFile::copy(getLibFileName(), getMetadataBackupPath() + "/" + "fundamental" + QDateTime::currentDateTime().toString(".yyyy.MM.dd.hh.mm.ss.zzz"));
	}
	QFile file(getLibFileName());
	if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
		qDebug() << __FILE__ << __LINE__ << "Failed to open file for writing:" << file.fileName();
		return false;
	}
	QDataStream out(&file);
	out.setVersion(QDataStream::Qt_4_0);

	out << *this;

	foreach (QByteArray docHash, docs.keys()) {
		savePageCache(docHash);
	}

	cleanupOldBackups();
	return true;
}

void Library::cleanupOldBackups()
{
	QDir dir;
	if (!dir.cd(getMetadataBackupPath()))
		return;

	// list files
	dir.setFilter(QDir::Files | QDir::Hidden | QDir::NoSymLinks | QDir::NoDotAndDotDot);
	QFileInfoList list = dir.entryInfoList();

	QStringList backups;

	QRegExp regexpBackup("fundamental\\.\\d\\d\\d\\d\\.\\d\\d\\.\\d\\d\\.\\d\\d\\.\\d\\d\\.\\d\\d\\.\\d\\d\\d");

	for (int i = 0; i < list.size(); i++) {
		QFileInfo fileInfo = list.at(i);
		QString fileName = fileInfo.fileName();

		if (regexpBackup.exactMatch(fileName)) {
			backups << fileName;
		}
	}
	qSort(backups);

	if (backups.count() <= 3)
		return;

	backups.removeLast();
	backups.removeLast();
	backups.removeLast();

	foreach (QString b, backups) {
		dir.remove(b);
	}
}

const QHash<QByteArray, LibraryDoc> Library::getDocs()
{
	return docs;
}

const QString Library::getPath()
{
	return path;
}

const QString Library::getLibFileName()
{
	return path + "/" + libName;
}

const QString Library::getDocFileName(QByteArray docHash)
{
	return path + "/" + docHash.toHex() + ".pdf";
}

const QString Library::getDownloadsPath()
{
	return path + "/" + "Downloads";
}

const QString Library::getDeletedPath()
{
	return path + "/" + "Deleted";
}

const QString Library::getMetadataBackupPath()
{
	return path + "/" + "MetadataBackup";
}

bool Library::addFile(LibraryDoc doc)
{
	if (docs.contains(doc.getHash())) {
		return false;
	}
	docs[doc.getHash()] = doc;
	QString message;
	runCommand("git",
			   QStringList() << "add" << doc.getHash() + ".pdf",
			   path,
			   message);
	return true;
}

bool Library::deleteFile(QByteArray docHash)
{
	if (!docs.contains(docHash)) {
		return false;
	}
	QString fileName = getDocFileName(docHash);
	QString destName = getDeletedPath() + "/" + docHash.toHex() + ".pdf";
	if (!QFile::copy(fileName, destName) && !QFile(destName).exists()) {
		qDebug() << QString("Could not copy file from: %1 to: %2").arg(fileName).arg(destName);
		return false;
	}
	docs.remove(docHash);
	return true;
}

QString Library::getFileData(QByteArray docHash, QString key)
{
	if (!docs.contains(docHash))
		return QString();
	return docs[docHash].getValue(key);
}

void Library::setFileData(QByteArray docHash, QString key, QString value)
{
	if (!docs.contains(docHash))
		return;
	QString oldValue = docs[docHash].getValue(key);
	if (oldValue == value)
		return;
	docs[docHash].setValue(key, value);
}

QList<TextNoteAnnotation> Library::getTextNoteAnnotations(QByteArray docHash)
{
	if (!docs.contains(docHash))
		return QList<TextNoteAnnotation>();
	return docs[docHash].getTextNoteAnnotations();
}

TextNoteAnnotation Library::addTextNoteAnnotation(QByteArray docHash, int pageNumber, QPointF pos, QColor color)
{
	if (!docs.contains(docHash))
		return TextNoteAnnotation();
	return docs[docHash].addTextNoteAnnotation(pageNumber, pos, color);
}

void Library::deleteTextNoteAnnotation(QByteArray docHash, int id)
{
	if (!docs.contains(docHash))
		return;
	docs[docHash].deleteTextNoteAnnotation(id);
}

void Library::changeTextNoteAnnotationTitle(QByteArray docHash, int id, QString title)
{
	if (!docs.contains(docHash))
		return;
	docs[docHash].changeTextNoteAnnotationTitle(id, title);
}

void Library::changeTextNoteAnnotationText(QByteArray docHash, int id, QString text)
{
	if (!docs.contains(docHash))
		return;
	docs[docHash].changeTextNoteAnnotationText(id, text);
}

void Library::changeTextNoteAnnotationColor(QByteArray docHash, int id, QColor color)
{
	if (!docs.contains(docHash))
		return;
	docs[docHash].changeTextNoteAnnotationColor(id, color);
}

void Library::changeTextNoteAnnotationPos(QByteArray docHash, int id, QPointF pos)
{
	if (!docs.contains(docHash))
		return;
	docs[docHash].changeTextNoteAnnotationPos(id, pos);
}

void Library::changeTextNoteAnnotationMaximized(QByteArray docHash, int id, bool maximized)
{
	if (!docs.contains(docHash))
		return;
	docs[docHash].changeTextNoteAnnotationMaximized(id, maximized);
}

void Library::changeTextNoteMaximumSize(QByteArray docHash, int id, QSize maximumSize)
{
	if (!docs.contains(docHash))
		return;
	docs[docHash].changeTextNoteAnnotationMaximumSize(id, maximumSize);
}

QString Library::getReportText(QByteArray docHash)
{
	if (!docs.contains(docHash))
		return QString();
	return docs[docHash].getReportText();
}

void Library::changeReportText(QByteArray docHash, QString text)
{
	if (!docs.contains(docHash))
		return;
	docs[docHash].changeReportText(text);
}

void Library::setViewOffset(QByteArray docHash, qreal offsetNormalized)
{
	if (!docs.contains(docHash))
		return;
	docs[docHash].setViewOffset(offsetNormalized);
}

qreal Library::getViewOffset(QByteArray docHash)
{
	if (!docs.contains(docHash))
		return 0;
	return docs[docHash].getViewOffset();
}

QHash<RenderCacheKey, QPair<QImage, qint64> > Library::getPageCache(QByteArray docHash)
{
	if (!docs.contains(docHash))
		return QHash<RenderCacheKey, QPair<QImage, qint64> >();
	return docs[docHash].pageCache;
}

void Library::setPageCacheDirty(QByteArray docHash)
{
	if (docs.contains(docHash))
		docs[docHash].pageCacheDirty = true;
}

void Library::setPageCache(QByteArray docHash, QHash<RenderCacheKey, QPair<QImage, qint64> > pageCache)
{
	if (docs.contains(docHash))
		docs[docHash].pageCache = pageCache;
}

void Library::loadPageCache(QByteArray docHash)
{
	if (!docs.contains(docHash))
		return;
	if (!docs[docHash].pageCache.isEmpty())
		return;

	QFile file(getPath() + "/" + docHash.toHex() + ".cache");
	if (!file.open(QIODevice::ReadOnly)) {
		return;
	}
	QDataStream in(&file);
	in.setVersion(QDataStream::Qt_4_0);

	quint32 version;
	in >> version;
	if (version != 1 || in.status() != QDataStream::Ok)
		return;

	in >> docs[docHash].pageCache;
	if (in.status() != QDataStream::Ok)
		docs[docHash].pageCache.clear();
	docs[docHash].pageCacheDirty = false;
}

void Library::savePageCache(QByteArray docHash)
{
	if (!docs.contains(docHash))
		return;
	if (docs[docHash].pageCache.isEmpty())
		return;
	if (!docs[docHash].pageCacheDirty)
		return;

	QFile file(getPath() + "/" + docHash.toHex() + ".cache");
	if (!file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
		return;
	}
	QDataStream out(&file);
	out.setVersion(QDataStream::Qt_4_0);

	quint32 version = 1;
	out << version;

	out << docs[docHash].pageCache;
	docs[docHash].pageCacheDirty = false;
}

QHash<int, QList<HighlightAnnotation> > Library::getHighlights(QByteArray docHash)
{
	if (!docs.contains(docHash))
		return QHash<int, QList<HighlightAnnotation> >();
	return docs[docHash].getHighlights();
}

HighlightAnnotation Library::createHighlight(QByteArray docHash, int pageNumber, QRectF box, QColor color)
{
	if (!docs.contains(docHash))
		return HighlightAnnotation();
	return docs[docHash].createHighlight(pageNumber, box, color);
}

void Library::deleteHighlight(QByteArray docHash, int pageNumber, QRectF box)
{
	if (!docs.contains(docHash))
		return;
	docs[docHash].deleteHighlight(pageNumber, box);
}

// expands abbreviations such as 'y' into 'year' etc.
QString expandQueryKey(QString key)
{
	if (key == "content") {
		return "text";
	} else if (key == "t") {
		return "title";
	} else if (key == "a") {
		return "author";
	} else if (key == "authors") {
		return "author";
	} else if (key == "p") {
		return "published in";
	} else if (key == "publication") {
		return "published in";
	} else if (key == "y") {
		return "year";
	} else if (key == "tag") {
		return "tags";
	} else {
		return key;
	}
}

QString capitalize(QString s)
{
	if (s.isEmpty())
		return s;
	return s.mid(0, 1).toUpper() + s.mid(1);
}

// parse qury
// query like: what the matrix is title:matrix author:brothers 'published in':cinema worldwide y:1999 tags:kung fu
// parse query into (key, value) pairs
QHash<QString, QString> parseQuery(QString query)
{
	QHash<QString, QString> parsedQuery;

//	query = "title:from";

	QString key = "Any";
	QStringList value;

	QString token;
	bool inQuote = false;
	while (!query.isEmpty()) {
		if (query.at(0) == '\'') {
			inQuote = !inQuote;
		} else if (!inQuote && query.at(0) == ' ') {
			// add token to value, unless it is empty
			if (!token.isEmpty()) {
				value << token;
				token.clear();
			}
		} else if (!inQuote && query.at(0) == ':') {
			// token is a key; store the current (key, value) pair
			if (!value.isEmpty()) {
				parsedQuery[capitalize(expandQueryKey(key))] = value.join(" ");
			}
			// set key to token
			key = token;
			value.clear();
			token.clear();
		} else {
			token += query.at(0);
		}
		query = query.mid(1);
	}
	// add token to value, unless it is empty
	if (!token.isEmpty()) {
		value << token;
		token.clear();
	}
	// store the current (key, value) pair
	if (!value.isEmpty()) {
		parsedQuery[capitalize(expandQueryKey(key))] = value.join(" ");
		key.clear();
		value.clear();
	}

	return parsedQuery;
}

qreal matchScore(QString docValue, QString queryValue, QString &keyMatchText)
{
	keyMatchText.clear();
	qreal score = 0;

	QStringList queryValueTokens = queryValue.split(' ', QString::SkipEmptyParts);

	for (int len = 1; len <= queryValueTokens.count(); len++) {
		for (int pos = 0; pos + len <= queryValueTokens.count(); pos++) {
			QString needle = QStringList(queryValueTokens.mid(pos, len)).join(" ");
			if (docValue.contains(needle, Qt::CaseInsensitive)) {
				score += len;
				keyMatchText = needle;
			}
		}
	}

	return score;
}

QDebug operator<<(QDebug dbg, const LibrarySearchResult &r)
{
	dbg.nospace() << "(LibrarySearchResult score=" << r.score << ", match=" <<r.displayText << "doc=" << r.doc << ")";
	return dbg.space();
}

bool scoreCompare(const LibrarySearchResult &r1, const LibrarySearchResult &r2)
{
	return r1.score > r2.score;
}

QList<LibrarySearchResult> Library::search(QString query)
{
	if (query.contains("|")) {
		QStringList queryParts = query.split("|", QString::SkipEmptyParts);
		QHash<QByteArray, LibrarySearchResult> results;
		foreach (QString queryPart, queryParts) {
			foreach (LibrarySearchResult result, search(queryPart)) {
				if (results.contains(result.doc.getHash())) {
					results[result.doc.getHash()].score += result.score;
				} else {
					results[result.doc.getHash()] = result;
				}
			}
		}

		QList<LibrarySearchResult> resultList = results.values();
		qSort(resultList.begin(), resultList.end(), scoreCompare);
		return resultList;
	}

	if (query.contains("&")) {
		QStringList queryParts = query.split("&", QString::SkipEmptyParts);
		QHash<QByteArray, LibrarySearchResult> results;
		bool first = true;
		foreach (QString queryPart, queryParts) {
			QHash<QByteArray, LibrarySearchResult> partialResults;
			foreach (LibrarySearchResult result, search(queryPart)) {
				partialResults[result.doc.getHash()] = result;
			}
			if (first) {
				results = partialResults;
				first = false;
			} else {
				foreach (QByteArray hash, partialResults.uniqueKeys()) {
					if (results.contains(hash)) {
						results[hash].score += partialResults[hash].score;
					}
				}
				foreach (QByteArray hash, results.uniqueKeys()) {
					if (!partialResults.contains(hash)) {
						results.remove(hash);
					}
				}
			}
		}

		QList<LibrarySearchResult> resultList = results.values();
		qSort(resultList.begin(), resultList.end(), scoreCompare);
		return resultList;
	}

	QList<LibrarySearchResult> results;
	QHash<QString, QString> parsedQuery = parseQuery(query);

	qDebug() << parsedQuery;

	foreach (LibraryDoc doc, docs.values()) {
		QString matchText;
		qreal score = 0;
		foreach (QString key, parsedQuery.keys()) {
			QStringList lookupKeys;
			if (key == "Any") {
				lookupKeys = doc.metadata.keys();
			} else {
				lookupKeys << key;
			}
			foreach (QString lookupKey, lookupKeys) {
				QString docValue = doc.getValue(lookupKey);
				QString queryValue = parsedQuery[key];
				QString keyMatchText;
				qreal keyScore = matchScore(docValue, queryValue, keyMatchText);
				if (keyScore > 0) {
					// qDebug() << keyScore << docValue << queryValue << keyMatchText;
					score += keyScore;
					matchText += capitalize(lookupKey) + ": " + keyMatchText + " ";
				}
			}
		}
		if (score > 0) {
			matchText = matchText.trimmed();
			LibrarySearchResult r;
			r.score = score;
			r.doc = doc;
			r.displayText = matchText;
			results << r;
		}
	}

	qSort(results.begin(), results.end(), scoreCompare);

	return results;
}

void Library::setOpen(QByteArray docHash)
{
	if (!docs.contains(docHash))
		return;
	if (openDocs.contains(docHash))
		return;
	openDocs << docHash;
	loadPageCache(docHash);
}

void Library::setClosed(QByteArray docHash)
{
	openDocs.removeOne(docHash);
}

QList<QByteArray> Library::getOpenDocs()
{
	return openDocs;
}

bool Library::setAutoGit(bool autoGit, QString &message)
{
	message.clear();
	if (!this->autoGit && autoGit) {
		if (!isGitRepoClean(message)) {
			return false;
		}
	}
	this->autoGit = autoGit;
	return true;
}

bool Library::getAutoGit()
{
	return autoGit;
}

bool Library::detectGit()
{
	QFile file(path + QDir::separator() + ".git");
	return file.exists();
}

bool waitForStarted(int msecs = 30000);
bool QProcess_waitx(QProcess *process,
					bool (QProcess::*waitFunctionPointer)(int),
					const int timeout_ms)
{
	bool ok;
	const int step_ms = 100;
	for (int maxDuration_ms = timeout_ms >= 0 ? timeout_ms : 0; maxDuration_ms >= 0; maxDuration_ms -= (timeout_ms >= 0 ? step_ms : 0)) {
		ok = (process->*waitFunctionPointer)(step_ms);
		if (!ok && process->error() == QProcess::Timedout) {
			QCoreApplication::processEvents();
			continue;
		} else {
			break;
		}
	}
	return ok;
}

bool runCommand(QString application, QStringList args, QString dir, QString &message, QString *output)
{
	qDebug() << QString("Running:") <<
				application <<
				args <<
				dir;
	message.clear();
	QProcess process;
	process.setWorkingDirectory(dir);
	process.start(application, args);
	if (!QProcess_waitx(&process, &QProcess::waitForStarted, 10000)) {
		message = QString("Could not start %1 %2.\n").
				  arg(application).
				  arg(args.join(" "));
		return false;
	}
	process.closeWriteChannel();
	QString out;
	if (!QProcess_waitx(&process, &QProcess::waitForFinished, -1)) {
		out = process.readAll();
		if (output) {
			*output = out;
		}
		message = QString("Error running %1 %2.\n").
				  arg(application).
				  arg(args.join(" "));
		message += QString("Return code: %1\n").arg(process.exitCode());
		message += QString("Error string: %1\n").arg(process.errorString());
		message += QString("Output:\n%1\n").arg(out);
		return false;
	}

	out = process.readAll();
	if (output) {
		*output = out;
	}
	if (process.exitCode() != 0) {
		message = QString("Error running %1 %2.\n").
				  arg(application).
				  arg(args.join(" "));
		message += QString("Return code: %1\n").arg(process.exitCode());
		message += QString("Error string: %1\n").arg(process.errorString());
		message += QString("Output:\n%1\n").arg(out);
		return false;
	}
	qDebug() << "Run OK";
	return true;
}

bool Library::gitPull(QString &message)
{
	message.clear();
	if (!detectGit()) {
		message = "Could not locate .git directory. Not a repository?";
		return false;
	}
	if (!isGitRepoClean(message)) {
		return false;
	}
	return runCommand("git", QStringList() << "pull", path, message);
}

bool Library::isGitRepoClean(QString &message)
{
	QString output;
	if (!runCommand("git", QStringList() << "status", path, message, &output)) {
		message = QString("Could not run git status. Message: %1\n").arg(message);
		return false;
	}
	if (!output.contains("On branch master")) {
		message = QString("It appears that the library git repository is not on branch master. This is not OK. "
						  "Actual git status output:\n%1\n").arg(output);
		return false;
	}
	if (output.contains("Your branch is ahead of")) {
		message = QString("It appears that the library git repository is ahead of remote. This is not OK. "
						  "Actual git status output:\n%1\n").arg(output);
		return false;
	}
	if (output.contains("Changes not staged for commit")) {
		message = QString("It appears that the library git repository has local changes. This is not OK. "
						  "Actual git status output:\n%1\n").arg(output);
		return false;
	}
	return true;
}

bool Library::gitCommitAndPush(QString &message)
{
	message.clear();
	if (!detectGit()) {
		message = "Could not locate .git directory. Not a repository?";
		return false;
	}

	bool ok = runCommand("git",
						 QStringList() << "add" << "*.pdf",
						 path,
						 message);
	if (!ok) {
		return false;
	}

	ok = runCommand("git",
					QStringList() << "commit" << "-a" << "-m" << "Auto sync" << "--allow-empty",
					path,
					message);
	if (!ok) {
		return false;
	}

	ok = runCommand("git",
					QStringList() << "push" << "origin" << "master",
					path,
					message);
	if (!ok) {
		return false;
	}
	return true;
}

QDataStream& operator<<(QDataStream& s, const Library& l)
{
	qint32 ver = 2;

	s << ver;
	s << l.docs;
	s << l.openDocs;

	return s;
}

QDataStream& operator>>(QDataStream& s, Library& l)
{
	qint32 ver;

	s >> ver;
	s >> l.docs;

	if (ver >= 2) {
		s >> l.openDocs;
	} else {
		l.openDocs.clear();
	}

	return s;
}

QDataStream& operator<<(QDataStream& s, const LibraryDoc& d)
{
	qint32 ver = 8;

	s << ver;
	s << d.sha1;
	s << d.metadata;
	s << d.textNoteAnnotations;
	s << d.highlights;
	s << d.reportText;
	s << d.viewOffsetNormalized;
	s << qCompress(d.fullText.toUtf8(), 9);

	return s;
}

QDataStream& operator>>(QDataStream& s, LibraryDoc& d)
{
	qint32 ver;

	s >> ver;
	s >> d.sha1;
	s >> d.metadata;
	if (ver >= 3) {
		s >> d.textNoteAnnotations;
	}
	if (ver >= 4) {
		s >> d.highlights;
	}
	if (ver >= 5) {
		s >> d.reportText;
	}
	if (ver >= 6) {
		s >> d.viewOffsetNormalized;
	} else {
		d.viewOffsetNormalized = 0;
	}
	if (ver == 7) {
		s >> d.fullText;
	}
	if (ver >= 8) {
		QByteArray b;
		s >> b;
		b = qUncompress(b);
		d.fullText = QString::fromUtf8(b.data());
	}

	return s;
}


LibraryDoc::LibraryDoc(QByteArray hash)
{
	sha1 = hash;
	pageCacheDirty = false;
}

LibraryDoc::LibraryDoc(const LibraryDoc &other)
{
	*this = other;
}

LibraryDoc &LibraryDoc::operator=(const LibraryDoc &other)
{
	sha1 = other.sha1;
	metadata = other.metadata;
	textNoteAnnotations = other.textNoteAnnotations;
	highlights = other.highlights;
	reportText = other.reportText;
	viewOffsetNormalized = other.viewOffsetNormalized;
	fullText = other.fullText;
	pageCache = other.pageCache;
	pageCacheDirty = other.pageCacheDirty;
	return *this;
}

bool LibraryDoc::isNull()
{
	return getHash().isEmpty();
}

QByteArray LibraryDoc::getHash() const
{
	return sha1;
}

QString LibraryDoc::getValue(QString key) const
{
	if (key == "Text")
		return fullText;
	return metadata.value(key, QString());
}

void LibraryDoc::setValue(QString key, QString value)
{
	if (key == "Title") {
		if (!value.isEmpty()) {
			// value = value.mid(0, 1).toUpper() + value.mid(1).toLower();
		}
	}
	metadata[key] = value;
}

QList<TextNoteAnnotation> LibraryDoc::getTextNoteAnnotations() const
{
	return textNoteAnnotations.values();
}

TextNoteAnnotation LibraryDoc::addTextNoteAnnotation(int pageNumber, QPointF pos, QColor color)
{
	int maxId = -1;
	foreach (int id, textNoteAnnotations.keys()) {
		maxId = qMax(maxId, id);
	}
	maxId++;
	textNoteAnnotations[maxId] = TextNoteAnnotation(getHash(), maxId, pageNumber);
	textNoteAnnotations[maxId].setPos(pos);
	textNoteAnnotations[maxId].setColor(color);
	return textNoteAnnotations[maxId];
}

void LibraryDoc::deleteTextNoteAnnotation(int id)
{
	if (!textNoteAnnotations.contains(id))
		return;
	textNoteAnnotations.remove(id);
}

void LibraryDoc::changeTextNoteAnnotationTitle(int id, QString title)
{
	if (!textNoteAnnotations.contains(id))
		return;
	textNoteAnnotations[id].setTitle(title);
}

void LibraryDoc::changeTextNoteAnnotationText(int id, QString text)
{
	if (!textNoteAnnotations.contains(id))
		return;
	textNoteAnnotations[id].setRichText(text);
}

void LibraryDoc::changeTextNoteAnnotationColor(int id, QColor color)
{
	if (!textNoteAnnotations.contains(id))
		return;
	textNoteAnnotations[id].setColor(color);
}

void LibraryDoc::changeTextNoteAnnotationPos(int id, QPointF pos)
{
	if (!textNoteAnnotations.contains(id))
		return;
	textNoteAnnotations[id].setPos(pos);
}

void LibraryDoc::changeTextNoteAnnotationMaximized(int id, bool maximized)
{
	if (!textNoteAnnotations.contains(id))
		return;
	textNoteAnnotations[id].setMaximized(maximized);
}

void LibraryDoc::changeTextNoteAnnotationMaximumSize(int id, QSize maximumSize)
{
	if (!textNoteAnnotations.contains(id))
		return;
	textNoteAnnotations[id].setMaximumSize(maximumSize);
}

QString LibraryDoc::getReportText()
{
	return reportText;
}

void LibraryDoc::changeReportText(QString text)
{
	reportText = text;
}

void LibraryDoc::setViewOffset(qreal offsetNormalized)
{
	viewOffsetNormalized = offsetNormalized;
}

qreal LibraryDoc::getViewOffset()
{
	return viewOffsetNormalized;
}

QHash<QString, QString> LibraryDoc::getMetadata()
{
	return metadata;
}

QString LibraryDoc::getFullText()
{
	return fullText;
}

void LibraryDoc::setFullText(QString fullText)
{
	this->fullText = fullText;
}

QHash<int, QList<HighlightAnnotation> > LibraryDoc::getHighlights() const
{
	return highlights;
}

HighlightAnnotation LibraryDoc::createHighlight(int pageNumber, QRectF box, QColor color)
{
	for (int i = 0; i < highlights[pageNumber].count(); i++) {
		if (highlights[pageNumber][i].getBBox().contains(box.center())) {
			highlights[pageNumber][i].setColor(color);
			return highlights[pageNumber][i];
		}
	}
	HighlightAnnotation h(getHash(), pageNumber, box, color);
	highlights[pageNumber].append(h);
	return h;
}

void LibraryDoc::deleteHighlight(int pageNumber, QRectF box)
{
	for (int i = 0; i < highlights[pageNumber].count(); i++) {
		if (highlights[pageNumber][i].getBBox().contains(box.center())) {
			highlights[pageNumber].removeAt(i);
			return;
		}
	}
}

QDebug operator<<(QDebug dbg, const LibraryDoc &d)
{
	dbg.nospace() << "(LibraryDoc hash=" << d.getHash().toHex() << ", ";
	foreach (QString key, d.metadata.keys()) {
		dbg.nospace() << key << "=" << d.metadata[key] << ", ";
	}
	dbg.nospace() << ")";
	return dbg.space();
}

QString keyFromEndNoteKey(QString key)
{
	if (key == "%A") return "Author";
	if (key == "%B") return "Published in";
	if (key == "%C") return "Place Published";
	if (key == "%D") return "Year";
	if (key == "%E") return "Editor/Secondary Author";
	if (key == "%F") return "Label";
	if (key == "%G") return "Language";
	if (key == "%H") return "Translated Author";
	if (key == "%I") return "Publisher";
	if (key == "%J") return "Published in";
	if (key == "%K") return "Keywords";
	if (key == "%L") return "Call Number";
	if (key == "%M") return "Accession Number";
	if (key == "%N") return "Number (Issue)";
	if (key == "%P") return "Pages";
	if (key == "%Q") return "Translated Title";
	if (key == "%R") return "Electronic Resource Number";
	if (key == "%S") return "Tertiary Title";
	if (key == "%T") return "Title";
	if (key == "%U") return "URL";
	if (key == "%V") return "Volume";
	if (key == "%W") return "Database Provider";
	if (key == "%X") return "Abstract";
	if (key == "%Y") return "Tertiary Author";
	if (key == "%Z") return "Notes";
	if (key == "%0") return "Type";
	if (key == "%6") return "Number of Volumes";
	if (key == "%7") return "Edition";
	if (key == "%8") return "Date";
	if (key == "%9") return "Type of Work";
	if (key == "%?") return "Subsidiary Author";
	if (key == "%@") return "ISBN/ISSN";
	if (key == "%!") return "Short Title";
	if (key == "%&") return "Section";
	if (key == "%(") return "Original Publication";
	if (key == "%)") return "Reprint Edition";
	if (key == "%*") return "Reviewed Item";
	if (key == "%+") return "Author Address";
	if (key == "%^") return "Caption";
	if (key == "%>") return "Link to PDF";
	if (key == "%<") return "Research Notes";
	if (key == "%[") return "Access Date";
	if (key == "%=") return "Last Modified Date";
	if (key == "%~") return "Name of Database";
	// my custom keys
	if (key == "%Citations") return "Citations";
	return QString();
}

QHash<QString, QString> metadataFromEndNote(QString metadata)
{
	QHash<QString, QString> tags;
	foreach (QString line, metadata.split('\n')) {
		line = line.trimmed();
		if (line.isEmpty())
			continue;
		if (!line.startsWith('%')) {
			continue;
		}
		QString key = line.split(' ', QString::SkipEmptyParts).first();
		QString parsedKey = keyFromEndNoteKey(key);
		if (parsedKey.isEmpty())
			continue;
		QString value = line.mid(key.length()).trimmed();
		if (value.isEmpty())
			continue;
		if (tags.contains(parsedKey)) {
			tags[parsedKey] += ", " + value;
		} else {
			tags[parsedKey] = value;
		}
	}
	return tags;
}
